Skip to content

docs(guide): repair the seven ${…} sites on keys with no carriage row (objectui#7872) - #7900

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-7872-uncarried-expression-sites-three-pages
Sep 6, 2026
Merged

docs(guide): repair the seven ${…} sites on keys with no carriage row (objectui#7872)#7900
os-sam merged 1 commit into
mainfrom
claude/issue-7872-uncarried-expression-sites-three-pages

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #7872

Seven ${…} sites in three guide pages were written on keys with no carriage row, so the renderer received the characters the author typed. Repaired per passage under the 2026-09-01 fork-B ruling on #7115, never one blanket rule.

Measurement 1 — is #7868's pin corpus-count-sensitive? NO, so scripts/ is untouched

scripts/__tests__/check-doc-expression-carriage.test.ts asserts against fixtures, and its one live-corpus case says in a comment why it deliberately does not pin the count:

// ⛔ Deliberately NOT asserted: how many findings there are. That number moves
// with every card in this class, and pinning it here would make this gate
// blocking through the back door — the exact thing the ruling forbade.

Its vacuity guards are fences > 100 and nodes > 100 (204 and 361 here), and CONTROL_FIXTURES are inline string literals in the gate itself, not read from content/docs. Confirmed by running, not only by reading: all 28 cases pass after the corpus count moved, so the file surface is the three doc pages alone.

⚠️ Measurement 6 — the census reads 11 → 4, not 17 → 10. Reporting, not reconciling

The card's headline of 17 was measured on 59df1dab2, before PR #7847 landed. On this branch's base 655afabf2 the census already read 11#7847 repaired 6 of guide/expressions.md's 8 sites in the meantime. Removing this card's 7 therefore lands on 4, and those 4 are exactly the remainder the dispatch enumerated:

So the attribution is right and the corpus is intact; only the starting number was stale. Nothing was adjusted to make the arithmetic come out — no number is pinned anywhere in the repo (git grep uncarried finds only the gate's own prose).

The complete new census line, on 74f19969a:

Scanned 184 file(s) under content/docs: 204 json/jsonc fence(s), 204 parsed, 0 UNPARSED (1 parsed as an object body).
361 node(s) with a string `type`; 65 ${…} site(s) on those nodes, 61 of them carried.
By fence language: json 198 (198 parsed, 0 unparsed); jsonc 6 (6 parsed, 0 unparsed)
✅  Dialect blind spot: none — no fence OUTSIDE json/jsonc parses as a JSON document holding a typed node.
✅  Blind spot: none — every fence above was parsed and judged.

⚠️  4 site(s) in 3 page(s) author ${…} on a key nothing evaluates:

Measurement 5 — the carriage map, re-read from the artifact

Read out of the built @objectstack/spec@17.2.0 dist/ui/index.mjs, the lookup SchemaRenderer.tsx:1139 consumes, rather than copied from the card's table:

var EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT = Object.freeze({
  statistic: Object.freeze(["label", "value", "description"]),
  card: Object.freeze(["title", "description"]),
  button: Object.freeze(["label"])
});
var NO_EXPRESSION_BINDABLE_TEXT_KEYS = Object.freeze([]);

Three types carry anything at all. expressionBindableTextKeysFor('badge'), ('alert') and ('page') each return the frozen empty array — matching the card. Channels, derived from SchemaRenderer's own call sites: content · bags properties, props · conditions disabled, disabledOn, hidden, hiddenOn, visibility, visible, visibleOn, visibleWhen.

Measurement 3 — both claims re-verified on current main, and one extended

Not taken on trust from #7847:

  • text is not a BadgeSchema key. packages/types/src/data-display.ts:93 and the zod mirror zod/data-display.zod.ts:54 both declare label / variant / icon / children; renderers/data-display/badge.tsx renders schema.label || renderChildren(schema.body). Rendered through the real renderer, { type: 'badge', text: '${status}' } produces an empty badge with text="${status}" left on the element — SchemaRenderer spreads the node's post-strip top level as React props, which is what collectUnevaluatedExpressions scans for the "Unevaluated expression reached the DOM" diagnostic.
  • success / warning / info are not badge variants. Closed set default | secondary | destructive | outline, agreeing in three places: the TS union, the zod enum, and the renderer's own registration inputs.

Newly measured here, which the card left open ("Not asserted for alert"):

  • message is not an AlertSchema key either. AlertSchema declares title / description / variant / icon / dismissible / children; renderers/data-display/alert.tsx renders schema.title and schema.description || renderChildren(schema.body). So alert.message was the same defect as badge.text, not merely an unevaluated expression.
  • AlertSchema.variant is the closed set default | destructive — only two members. The old Complex Logic site computed info / warning / success and the old Error States site authored the literal error; all four are off-enum.

The seven positions, located by content on 655afabf2

page line node.key repair class
guide/schema-rendering.md 186 badge.text demonstrating binding → moved to a carrying key
guide/schema-rendering.md 187 badge.variant same passage
guide/schema-rendering.md 207 alert.variant demonstrating binding → moved to a carrying key
guide/schema-rendering.md 416 alert.message demonstrating binding → moved to a carrying key
guide/architecture.md 257 badge.text demonstrating binding → moved to a carrying key
guide/architecture.md 258 badge.variant same passage
guide/layout.md 511 page.title incidental → literal plus prose

Six of the seven sit under headings that are teaching binding — "Conditional Expressions", "Complex Logic", "Error States", "Data Transformations" — so de-expressing them would have deleted the lesson. They move onto keys that genuinely carry: card.title / card.description, statistic.label / value / description, and a nested text node's content, which SchemaRenderer evaluates on every node type (if (typeof newSchema.content === 'string'), no type gate). The nested form is the honest escape hatch for a non-carrying component: alert's renderer falls back from description to renderChildren(schema.body), and renderChildren routes the child through SchemaRenderer, so the inner content is evaluated. Each repaired fence also carries prose naming the missing row and the working alternative, matching how #7115 rewrote progress and input.

layout.md's site is the one incidental case — the passage is about page structure with actions, not about binding — so its title becomes a literal with prose pointing at the host and at a card in the body.

Bounded in-place fix, declared

layout.md's same fence spelled ${record.name} a second time, in a breadcrumb item's label. The census cannot see it (a breadcrumb item is not a typed node) and it is not one of the seven. It is made literal anyway, because the prose this PR adds four lines above says to resolve record.name in the host — leaving the expression in place would have contradicted the sentence being added. Whether breadcrumb labels are evaluated was not measured; a literal is correct either way, which is why this repair does not depend on the answer.

The docs' bare root spelling is host scope, not a defect — measured before deciding

Rendering the repaired fences with only a dataSource mounted, nothing resolved: ${status} came back verbatim while ${data.status} gave "active". That was the harness, not the pages. SchemaRenderer builds its evaluator as { ...predicateScope, current_user, [record], data: dataSource, page }, and PredicateScopeProvider takes an arbitrary Record of top-level names that app-shell's ExpressionProvider feeds. So bare ${status} / ${orders.length} resolve when the host binds them — and it is the convention #7847 kept throughout expressions.md (${user.firstName}, ${score >= 90 ? …}). The spelling is therefore left alone; changing it would have been an unmeasured rewrite.

Verification

Every repaired fence is parsed out of the shipped page and rendered through the real production renderers with PredicateScopeProvider + SchemaRendererProvider mounted — 8 cases, all green: the card renders Active / This record is in use., the Complex Logic alert renders Welcome! / You are all caught up., the Error States alert renders Upstream timed out, the statistic renders 12 / Above target, no ${ survives in any DOM, and layout.md's fence holds no ${…} at all. Three negative cases pin the old shapes: an empty badge with ${status} on the DOM, badge.label rendering correctly, and alert.message leaving the description slot empty.

Ablation — with the pre-repair pages restored onto disk from 655afabf2 (mutation confirmed on disk by grep count, 1/1/1 present and the repaired statistic fence 0), the same harness went 5 failed | 3 passed: the five repaired-site cases fail and the three old-shape cases pass, which is the instrument seeing the difference rather than a check that cannot fail. The restore leg was proven byte-identical — each file's git hash-object equals its HEAD blob (61257c6cb, 35c560cf5, f54c7cd88) and git diff HEAD is empty — and the harness returned to 8/8 afterwards. The harness itself is a throwaway and is not part of this PR; git status is clean.

Gate union re-run after the final commit, on 74f19969a, with git diff HEAD empty. Exit codes captured by redirecting first, never through a pipe.

gate exit verdict line
check-doc-expression-carriage 0 ✅ Controls pass: this gate can see the class it is looking for, and does not cry wolf.
its 28-case pin test 0 Test Files 1 passed (1) · Tests 28 passed (28)
check:doc-types 0 ✅ Every documented component type is registered.
check:doc-fences 0 ✅ check:doc-fences — every TypeScript block in 227 document(s) is fenced ts/tsx/typescript …
check:doc-snippets 0 Semantic phase: 456 of 456 block(s) judged, 0 failed.
check:doc-example-readers 0 OK 80 documented symbol(s), 3945 call site(s) … no @example hand-spells one.
check:docs-route-closure 0 route closure recorded, no new eager edge
check-changeset-presence 0 ✅ No source or published contract of a released package changed in this range, so no changeset is owed.
governed-surface guard 0 ✅ NOT GOVERNED — 3 path(s) checked against 5 governed surface(s); none matched.

check:doc-snippets first answered EXIT=2 (precondition not met — packages unbuilt). Its scoped closure was built with pnpm exec turbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter) --concurrency=2 (34 tasks, all successful) and it was re-run for the real green above.

Control-byte self-scan of the three edited files: grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' matched nothing.

Not run locally, declared: the repo-wide pnpm lint and the rest of the CI farm. CI runs the farm exactly once regardless; the gates above are the family that reads the files this diff changes.

Out of scope, filed

#7898 — measured in these same pages while working here, and not repaired: six button nodes author text (not a ButtonSchema key; the renderer reads label) and three fences author a top-level onClick (handlerKeyRefusal(..., 'runtime-slot'), refused by name). Both hold literals, so the carriage census cannot see them and check:doc-types judges only the type literal. quick-start.md:149's onClick is recorded there as measured-and-NOT-a-defect: it sits inside the events bag, which is the correct dialect.

Untouched, as instructed: guide/expressions.md, schema-rendering.md's chart.data site, api/schema-reference.md, and scripts/check-doc-expression-carriage.mjs's predicate and scan population.

🤖 Generated with Claude Code


Generated by Claude Code

… (objectui#7872)

`SchemaRenderer` evaluates `content`, the `properties`/`props` bags, the eight
condition keys, and the carriage rows `expressionBindableTextKeysFor` gives the
node's own type. `badge`, `alert` and `page` have no rows, so every `${…}` these
three pages wrote on `badge.text`, `badge.variant`, `alert.variant`,
`alert.message` and `page.title` reached the renderer as the characters the
author typed.

Repaired per passage under the 2026-09-01 fork-B ruling on objectui#7115, not by
one blanket rule: the passages demonstrating binding keep demonstrating it on a
key that genuinely carries (`card.title`/`description`, `statistic.label`/
`value`/`description`, a nested `text` node's `content`), and the page-layout
passage takes a literal plus prose naming the missing row.

Two of the old shapes were broken beyond not evaluating, re-measured here
through the real renderers rather than carried over from objectui#7847:
`text` is not a `BadgeSchema` key (the badge's text is `label`, so the old shape
rendered an empty badge with `text=${…}` left on the DOM), and `success` /
`warning` / `info` are not badge variants. Newly measured on this card:
`message` is not an `AlertSchema` key either — the alert's text keys are `title`
and `description` — and `AlertSchema.variant` is the closed set `default` |
`destructive`, so `info` / `warning` / `success` / `error` are all off-enum.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(docs): 7 more ${…} sites on keys with no carriage row, in 3 pages no card covers — measured by the objectui#7851 census

2 participants